<?php

/**
 * \file
 * Classe `Entity`.
 */

/**
 * Entités de données.
 */ 
namespace pilar\Entities /* \cond */ ; /* \endcond */
{

require_once(__DIR__.'/config.php');

/**
 * Entité de données.
 *
 * \todo
 * Changer 'caption' en 'label' ?
 *
 * \todo
 * Faire que le membre 'id' soit auto généré quand on ne le fournit pas.
 *
 * \since 22/07/2008
 * \author Matthieu OVIEDO <matthieu.oviedo@gmail.com>
 */
class Entity extends \object {

/**
 * Membres.
 *
 * \todo
 * Rendre privé.
 */
public $members;


/**
 * Membre de titre.
*
 * \todo
 * Rendre privé.
 *
 * \since 22/09/2008.
 */
public $titleMember;


/**
 * Chemin d'accès du fichier touch associé à l'entité.
 *
 * \since 09/01/2008
 */
private $_touchFile = null;


/**
 * Sous-entités. Utilisé pour des processus d'installation,
 * de desinstallaion et d'upgrade.
 *
 * \since 27/04/2009
 */
private $_subEntities = array();


/**
 * Sous-entités chargés ?
 *
 * \since 27/04/2009
 */
private $_subEntitiesLoaded = false;


protected function loadDefaultParams() {
 parent::loadDefaultParams();

 $this->params->define(array(
  /**
   * Nom de l'entité.
   *
   * Exemple : 'elephant'.
   */
  'name' => '',
  
  /**
   * Nom de l'entité au pluriel.
   *
   * Exemple : 'elephants'.
   */
  'plName' => '',

  /**
   * Entités étendues par l'entité courante.
   * 
   * Elles doivent être données par ordre de priorité, avec les
   * identifiants d'extension comme clés.
   *
   * Les identifiants ne doivent pas se recouper avec un des
   * identifiants de membres.
   *
   * Par exemple :
   * \code
   * $ElephantEntity = new \pilar\Entities\Entity(array(
   *  ...,
   *  array(
   *   'animal' => $AnimalEntity,
   *   'transport' => $TransportEntity
   *  ),
   *  ...
   * ));
   * \endcode
   *
   * \since 08/03/2009
   */
  'extends' => array(),

  /**
   * Classe associée aux objets issus de l'entité.
   *
   * On suppose que la classe déscend de la classe
   * \c 'pilar\Object\Object' qui est la classe par défaut.
   *
   * \since 08/03/2009
   * \author Matthieu OVIEDO <matthieu.oviedo@gmail.com> 
   */
  'objectClass' => '\pilar\Objects\Object',

  /**
   * Intitulé de l'entité.
   *
   * Exemple : 'éléphant'.
   */
  'caption' => '',
  
  /**
   * Intitulé pluriel de l'entité.
   *
   * Exemple : 'éléphants'.
   */
  'plCaption' => '',

  /**
   * Genre de l'entité, \c 'he' pour masculin (défaut), ou \c 'she'
   * pour féminin).
   */
  'genre' => 'he',

  /**
   * Membres de l'entité.
   */
  'members' => array(),

  /**
   * Préfixe associé à l'entité, \c '' par défaut.
   *
   * Par suite pour un système donné on suppose que le couple
   * (prefix, name) identifier de manière unique l'entité.
   *
   * Par exemple : 'test' pour une entité de test.
   *
   * \todo
   * Mettre en place un système statique optionnel qui vérifie
   * surveille l'unicité des entités.
   */
  'prefix' => '',

  /**
   * Nom du membre à utiliser pour représenter les objets de cette
   * entité, \c 'id' par défaut.
   *
   * \todo
   * Mettre en place une manière plus élégante et plus souple (qui
   * ne se limite pas qu'à un seul membre).
   */
  'titleMemberName' => 'id',

  /**
   * Référence hierarchique. Utile pour des fils d'ariane ou autre
   * données qui ont besoin de connaître l'hierarchie des entités.
   * S'il y'en a pas, la chaîne vide signale ceci.
   * Le paramètre contient le nom du membre référence vers l'entité
   * en question.
   */
  'hierarchicalRef' => '',
  
  /**
   * Dossier d'enregistrement des fichiers touch associés à l'entité.
   *
   */
  'touchFilesDirectory' =>
    PILAR_ENTITIES_DEFAULT_TOUCHFILES_PATH
 ));
}


protected function initialize() {
 if ($this->get('plName') == '')
  $this->params->plName = $this->get('name').'s';
 
 parent::initialize();

 // Ajout des membres passés en paramètres :
  $this->addMembers($this->get('members'));

 // Si aucun membre ID n'a été créé alors on en crée un :
  if (!$this->memberExists('id')) {
   $this->addMember(new \pilar\Entities\Id());
  }
  
 // Membre de titre :
  $this->titleMember = $this->member(
   $this->params->titleMemberName
  );
}


/**
 * Permet d'ajouter des membres pour des cas pathologiques.
 */
public function loadAdditionalMembers() {
 // Ne fait rien dans le cas général.
}


/**
 * Ajoute un membre à l'entité. Si un membre avec le même nom existe
 * déjà il est remplacé.
 * 
 * \param $member Membre à ajouter.
 * \return $this
 */
public function addMember($member) {
 $this->members[$member->get('name')] = $member;
 $member->setParentEntity($this);
 return $this;
}


/**
 * Ajoute plusieurs membres à l'entité.
 *
 * Préférer l'utilisation de \a addMember si il ne s'agit que d'un
 * seul membre.
 *
 * \param $members Membres à ajouter.
 * \return $this
 *
 * \see addMember
 *
 * \since 08/03/2009
 * \author Matthieu OVIEDO <matthieu.oviedo@gmail.com>
 */
public function addMembers($members) {
 foreach($members as $member) {
  $this->addMember($member);
 }
}


/**
 * Recherche un membre en fonction de son nom, et génère une erreur
 * si le membre n'existe pas.
 *
 * \param $name Nom du membre.
 * \return Si le membre est trouvé il est retourné.
 */
public function member($name) {
 if (!$this->memberExists($name))
  trigger_error(
   "Le membre `{$name}` de l'entité `".$this->get('name')."` n'existe pas.",
   E_USER_ERROR
  );

 return $this->members[$name];
}


/**
 * Détermine si un membre existe avec un nom donné.
 *
 * \param $name Nom du membre en question.
 * \return \c TRUE si le membre existe, \c FALSE sinon.
 *
 * \since 08/03/2009
 * \author Matthieu OVIEDO <matthieu.oviedo@gmail.com> 
 */
public function memberExists($name) {
 return isset($this->members[$name]);
}


/**
 * Liste des champs de données de l'entité.
 *
 * \return Tableau contenant des références vers chaque champ.
 *
 * \since 09/09/2008
 */
public function fields() {
 /**
  * NOTE
  * Voir si il est nécessaire de mettre en cache la liste.
  */

 $fields = array();
 foreach($this->members as $member) {
  foreach($member->fields as $field) {
   $fields[] = $field;
  }
 }
 
 return $fields;
}


/**
 * Nom de la table SQL associée à l'entité.
 *
 * \return Nom de la table.
 */
public function sqlTableName() {
 if ($this->get('prefix') == '')
  return $this->get('name');
 else
  return $this->get('prefix').'_'.$this->get('name');
}


/**
 * Génère la commande SQL de création de la table SQL de l'entité.
 *
 * \return Chaine contenant la commande SQL `CREATE`.
 *
 * \since 09/09/2008
 */
public function sqlCreateTableCommand() {
 ob_start();
?>
CREATE TABLE `<?php echo $this->sqlTableName() ?>`
(
<?php foreach($this->fields() as $field) { ?>
 `<?php echo $field->fullName() ?>` <?php echo $field->sqlProperties() ?>,
<?php } ?>
 PRIMARY KEY(`idValue`)
) DEFAULT CHARSET=utf8;
<?php
 return ob_get_clean();
}


/**
 * Crée la table associée à l'entité.
 *
 * \todo
 * Faire en sorte que la fonction prenne en paramètre une connexion
 * à une base de données. Il n'est pas évident qu'une connexion
 * générale existe comme dans Toguna.
 *
 * \return $this
 *
 * \since 09/09/2008
 */
public function createTable() {
 \db\execute(
  $this->sqlCreateTableCommand()
 );

 return $this;
}


/**
 * Efface la table associée à l'entité si elle existe.
 *
 * \todo
 * Faire en sorte que la fonction prenne en paramètre une connexion
 * à une base de données. Il n'est pas évident qu'une connexion
 * générale existe comme dans Toguna.
 *
 * \return $this
 * 
 * \since 28/09/2008
 */
public function dropTable() {
 \db\execute(
  'DROP TABLE IF EXISTS `'.$this->sqlTableName().'`'
 );

 return $this;
}
	

/**
 * Détermine si la table associée à l'entité existe.
 *
 * \todo
 * Faire en sorte que la fonction prenne en paramètre une connexion
 * à une base de données. Il n'est pas évident qu'une connexion
 * générale existe comme dans Toguna.
 * 
 * \since 01/10/2008
 */
public function tableExists() {
 $result = \db\query(
  'SHOW TABLES LIKE \''.$this->sqlTableName().'\''
 );

 return !$result->isEmpty();
}


/**
 * Détermine si l'entité contient un champ avec un nom donné.
 *
 * \param $name Nom du champ.
 * \return \c TRUE si le champ existe, \c FALSE sinon.
 *
 * \since 05/10/2008
 */
public function fieldExists($name) {
 foreach ($this->fields() as $field) {
  if ($field->fullName() == $name) return true;
 }
 return false;
}


/**
 * Met à jour la table en base de données associée à l'entité. La fonction
 * suppose que la table existe en base de données.
 *
 * \return $this
 * 
 * \since 01/10/2008
 */
public function updateTable() {
 if (DB_CLEAN) {
 // On vérifie que toutes les colonnes de la table correspondent bien
 // à un champ de l'entité :
  // On vérifie les champs dans la table et on les renomme s'ils ne
  // correspondent à aucune colonne :
   $result = \db\query('SHOW COLUMNS FROM `'.$this->sqlTableName().'`');
   while ($record = $result->fetchArray()) {
    if (!$this->fieldExists($record[0])) {
     \db\execute(
      'ALTER TABLE `'.$this->sqlTableName().'` DROP `'.$record[0].'`'
     );
    }
   }
 }
 // On met à niveau toutes les colonne en base de données champ par champ :
  foreach($this->fields() as $field) {
   $field->upgradeColumn();
  }

 return $this;
}


/**
 * Met à niveau la table en base de données associée à l'entité.
 * La table est créée si elle n'existe pas, et mise à jour sinon.
 *
 * \return $this
 * 
 * \since 01/10/2008
 */
public function upgradeTable() {
 if ($this->tableExists()) {
  return $this->updateTable();
 } else {
  return $this->createTable();
 }
}


/**
 * Insère un objet dans la table SQL associée à l'entité.
 *
 * \todo
 * Cette fonction est obsolète. La retirer.
 *
 * \param $valuesTree Dictionnaire de valeurs en arbre sur 2 niveaux.
 */
public function insert($valuesTree = null) {
 if ($valuesTree == null) $valuesTree = array();

 // Construction des listes de champs et de valeurs :
  $fields = array();
  $values = array();
  foreach($valuesTree as $memberName => $value) {
   $member = $this->member($memberName);
   if (is_array($value)) { // Il s'agit d'un membre complexe ?
    foreach($value as $fieldName => $realValue) {
     $fields[] = '`'.$member->field($fieldName)->fullName().'`';
     $values[] = "'".\db\escape($realValue)."'";
    }    
   } else {
    $fields[] = '`'.$member->field()->fullName().'`';
    $values[] = "'".\db\escape($value)."'";
   }
  }
  
 // Construction de la requête SQL :
  $sql = 'INSERT INTO `'.$this->sqlTableName().'` ('.implode(', ', $fields).') VALUES ('.implode(', ', $values).')';
  
 // Exécution de la requête SQL :
  \db\execute($sql);
}


/**
 * Enlève un objet dans la table SQL associée à l'entité.
 *
 * \todo
 * Fonction obsolète. La retirer.
 * 
 * \since 27/09/2008
 * \params $id Identifiant de l'application dont les objets vont être 
 * enlevé.
 */
public function remove($id, $field) {
 \db\execute(
  "DELETE FROM `".$this->sqlTableName()."` WHERE `{$field}`='{$id}'"
 );
}


/**
 * Ajoute les champs, d'un membre donné, dans la table associée à
 * l'entité en base de données.
 *
 * \todo
 * Devrait être privée. De plus le nom `insertMember` n'est pas
 * adapté, et la fonction devrait prendre en argument un connexion
 * à une base de données.
 *
 * \param $member Membre pour lequel il faut ajouter le champ.
 *
 * \since 01/10/2008
 */
public function insertMember($member) {
 $isFirst = true;
 ob_start();
?>
ALTER TABLE `<?php echo $this->sqlTableName() ?>` ADD
(
<?php foreach($member->fields as $field) { ?>
<?php  if ($isFirst) $isFirst = false;
       else { ?>
,
<?php  } ?>
 `<?php echo $field->fullName() ?>` <?php echo $field->sqlProperties() ?>
<?php } ?>
)
<?php
 $sql = ob_get_clean();

 \db\execute($sql);
}


/**
 * Instancie un nouvel objet persistant.
 *
 * \todo
 * Si possible renommer cette méthode en "new" simplement pour
 * marquer l'analogie avec la programmation objet. De plus
 * cette méthode devrait retourner un objet adapté à l'entité
 * et non pas un objet générique.
 * 
 * \return Instance du nouvel objet.
 *
 * \since 07/10/2008
 */
public function newObject() {
 $class = $this->get('objectClass');
 return new $class($this);
}


/**
 * Nouvelle collection d'objets.
 *
 * \return Instance de la nouvelle collection d'objets.
 *
 * \since 08/10/2008
 */
public function newCollection() {
 return new \pilar\ObjectCollections\Collection($this);
}


/**
 * Recherche et retourne un objet persistant en fonction de son
 * identifiant en base de données.
 *
 * \todo
 * Utiliser une manière plus propre pour récupérer le premier objet.
 *
 * \param $id ID de l'objet.
 * \return Objet persistant correspondant à l'identifiant
 *         donné.
 *
 * \since 11/12/2008
 */
public function byId($id) {
 $objects = $this->newCollection();
 $objects->where(\eq($objects->id, \cst($id)));
 foreach($objects as $object) {
  return $object;
 }
}


/**
 * Libellé de l'entité avec un déterminant définit.
 *
 * \return Libellé déterminé.
 *
 * \since 03/12/2008
 */
public function determinedLabel() {
 // Cas où le mot commence par une voyelle :
  if (preg_match('/^[aeiouyé]/i', $this->get('caption'))) {
   return "l'".$this->get('caption');
  }
  
 // A ce stade on sait que le mot commence par une consonne.

 return  (($this->get('genre') == 'he')?'le':'la')
        .' '.$this->get('caption');
}


/**
 * Chemin d'accès du fichier touch associé à l'entité.
 *
 * Un cache interne est utilisé pour ne pas recalculer à chaque fois
 * le chemin d'accès complet.
 *
 * \return Chemin d'accès du fichier touch.
 * 
 * \since 09/01/2008
 */
public function touchFile() {
 if ($this->_touchFile == null) {
  $this->_touchFile = $this->params->touchFilesDirectory.
                      '/'.$this->params->name.'.touch';
 }

 return $this->_touchFile;
}


/**
 * "Touch" l'entité.
 *
 * \since 09/01/2008
 */
public function touch() {
 touch($this->touchFile());
}


/**
 * Entités étendues par l'entité courante.
 *
 * Elles sont données par ordre de priorité.
 *
 * \return Tableau contenant les entités étendues comme valeurs
 *         et leur identifiant d'extension comme clé.
 *
 * \since 21/02/2009
 */
public function extendedEntities() {
 return $this->get('extends');
}


/**
 * Ajout d'une sous-entité. Remplace une entité ayant le même nom.
 *
 * \param $entity Entité à ajouter.
 * \since 27/04/2009
 */
public function addSubEntity($entity) {
 $this->_subEntities[$entity->get('name')] = $entity;
}


/**
 * Accesseur des sous-entités. Force le chargement de celles-ci.
 *
 * \return Tableau contenant les sous-entités.
 * \since 27/04/2009
 */
public function subEntities() {
 if (!$this->_subEntitiesLoaded) {
  foreach ($this->members as $m) {
   $m->getSubEntities();
  }
  $this->_subEntitiesLoaded = true;
 }

 return $this->_subEntities;
}

} // Class.

} // Namespace.

?>
